{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# ipycanvas: Python canvas for Jupyter\n",
    "\n",
    "## https://github.com/martinRenou/ipycanvas\n",
    "\n",
    "Documentation: https://ipycanvas.readthedocs.io\n",
    "\n",
    "ipycanvas allows you to draw simple primitives directly from Python like text, lines, polygons, arcs, images etc. This simple toolset allows you to draw literally anything!\n",
    "\n",
    "- BSD-3-Clause License\n",
    "\n",
    "**Installation:**\n",
    "\n",
    "```bash\n",
    "conda install -c conda-forge ipycanvas\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Polygons and shapes"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from time import sleep\n",
    "from math import pi\n",
    "\n",
    "import numpy as np\n",
    "\n",
    "from ipycanvas import Canvas, MultiCanvas, RoughCanvas, hold_canvas"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "canvas = Canvas(width=200, height=200)\n",
    "\n",
    "canvas.fill_style = '#63934e'\n",
    "canvas.stroke_style = '#4e6393'\n",
    "canvas.line_width = 5\n",
    "canvas.fill_polygon([(20, 20), (180, 20), (100, 150)])\n",
    "canvas.stroke_polygon([(20, 20), (180, 20), (100, 150)])\n",
    "\n",
    "canvas"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "canvas = Canvas(width=150, height=150)\n",
    "\n",
    "canvas.fill_style = 'white'\n",
    "canvas.fill_rect(0, 0, 150, 150)\n",
    "canvas.fill_style = 'purple'\n",
    "\n",
    "# Cubic curves example\n",
    "canvas.begin_path()\n",
    "canvas.move_to(75, 40)\n",
    "canvas.bezier_curve_to(75, 37, 70, 25, 50, 25)\n",
    "canvas.bezier_curve_to(20, 25, 20, 62.5, 20, 62.5)\n",
    "canvas.bezier_curve_to(20, 80, 40, 102, 75, 120)\n",
    "canvas.bezier_curve_to(110, 102, 130, 80, 130, 62.5)\n",
    "canvas.bezier_curve_to(130, 62.5, 130, 25, 100, 25)\n",
    "canvas.bezier_curve_to(85, 25, 75, 37, 75, 40)\n",
    "canvas.fill()\n",
    "\n",
    "canvas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Lines"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "canvas = Canvas(width=200, height=200)\n",
    "\n",
    "canvas.fill_style = 'white'\n",
    "canvas.fill_rect(0, 0, 200, 200)\n",
    "\n",
    "n = 100\n",
    "x = np.linspace(0, 200, n)\n",
    "y = np.random.randint(200, size=n)\n",
    "\n",
    "points = np.stack((x, y), axis=1)\n",
    "\n",
    "canvas.stroke_lines(points)\n",
    "\n",
    "canvas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Fractal tree: Generate a new fractal tree on button click"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from math import pi\n",
    "from random import uniform\n",
    "\n",
    "from ipywidgets import Button\n",
    "\n",
    "canvas = Canvas(width=800, height=600)\n",
    "\n",
    "def recursive_draw_leaf(canvas, length, r_angle, r_factor, l_angle, l_factor):\n",
    "    canvas.stroke_line(0, 0, 0, -length)\n",
    "    canvas.translate(0, -length)\n",
    "\n",
    "    if length > 5 :\n",
    "        canvas.save()\n",
    "\n",
    "        canvas.rotate(r_angle)\n",
    "        recursive_draw_leaf(canvas, length * r_factor, r_angle, r_factor, l_angle, l_factor)\n",
    "\n",
    "        canvas.restore()\n",
    "\n",
    "        canvas.save()\n",
    "\n",
    "        canvas.rotate(l_angle)\n",
    "        recursive_draw_leaf(canvas, length * l_factor, r_angle, r_factor, l_angle, l_factor)\n",
    "\n",
    "        canvas.restore()\n",
    "\n",
    "\n",
    "def draw_tree(canvas):\n",
    "    with hold_canvas(canvas):\n",
    "        canvas.save()\n",
    "\n",
    "        canvas.clear()\n",
    "        \n",
    "        canvas.fill_style = 'white'\n",
    "        canvas.fill_rect(0, 0, 800, 600)\n",
    "\n",
    "        canvas.translate(canvas.width / 2., canvas.height)\n",
    "\n",
    "        canvas.stroke_style = 'black'\n",
    "\n",
    "        r_factor = uniform(0.6, 0.8)\n",
    "        l_factor = uniform(0.6, 0.8)\n",
    "\n",
    "        r_angle = uniform(pi / 10., pi / 5.)\n",
    "        l_angle = uniform(-pi / 5., -pi / 10.)\n",
    "\n",
    "        recursive_draw_leaf(canvas, 150, r_angle, r_factor, l_angle, l_factor)\n",
    "\n",
    "        canvas.restore()\n",
    "\n",
    "button = Button(description='Generate tree!')\n",
    "\n",
    "def click_callback(*args, **kwargs):\n",
    "    global canvas\n",
    "\n",
    "    draw_tree(canvas)\n",
    "\n",
    "button.on_click(click_callback)\n",
    "\n",
    "draw_tree(canvas)\n",
    "\n",
    "display(canvas)\n",
    "display(button)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Custom scatter plot"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import branca\n",
    "\n",
    "\n",
    "class ScatterPlot(MultiCanvas):\n",
    "    def __init__(self, x, y, size, color=None, scheme=branca.colormap.linear.RdBu_11, stroke_color='black'):\n",
    "        super(ScatterPlot, self).__init__(3, width=800, height=600, sync_image_data=True)\n",
    "\n",
    "        self.color = color\n",
    "        self.scheme = scheme\n",
    "        \n",
    "        self.background_color = '#f7f7f7'\n",
    "\n",
    "        self.init_plot(x, y)\n",
    "        \n",
    "        self.sizes = size\n",
    "        self.stroke_color = stroke_color\n",
    "\n",
    "        self.n_marks = min(x.shape[0], y.shape[0], size.shape[0], color.shape[0])\n",
    "\n",
    "        self.draw()\n",
    "\n",
    "    def init_plot(self, x, y, color=None, scheme=None):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "        self.color = color if color is not None else self.color\n",
    "        self.scheme = scheme if scheme is not None else self.scheme\n",
    "\n",
    "        padding = 0.1\n",
    "        padding_x = padding * self.width\n",
    "        padding_y = padding * self.height\n",
    "\n",
    "        self.drawarea = (drawarea_min_x, drawarea_min_y, drawarea_max_x, drawarea_max_y) = (padding_x, padding_y, self.width - 2 * padding_x, self.height - 2 * padding_y)\n",
    "\n",
    "        min_x, min_y, max_x, max_y = np.min(x), np.min(y), np.max(x), np.max(y)\n",
    "\n",
    "        dx = max_x - min_x\n",
    "        dy = max_y - min_y\n",
    "\n",
    "        # Turns a data coordinate into pixel coordinate\n",
    "        self.scale_x = lambda x: drawarea_max_x * (x - min_x) / dx + drawarea_min_x\n",
    "        self.scale_y = lambda y: drawarea_max_y * (1 - (y - min_y) / dy) + drawarea_min_y\n",
    "\n",
    "        # Turns a pixel coordinate into data coordinate\n",
    "        self.unscale_x = lambda sx: (sx - drawarea_min_x) * dx / drawarea_max_x + min_x\n",
    "        self.unscale_y = lambda sy: (1 - ((sy - drawarea_min_y) / drawarea_max_y)) * dy + min_y\n",
    "\n",
    "        self.colormap = None\n",
    "        if self.color is not None:\n",
    "            self.colormap = self.scheme.scale(np.min(self.color), np.max(self.color))\n",
    "\n",
    "    def draw_background(self):\n",
    "        drawarea_min_x, drawarea_min_y, drawarea_max_x, drawarea_max_y = self.drawarea\n",
    "\n",
    "        background = self[0]\n",
    "\n",
    "        # Draw background\n",
    "        background.fill_style = self.background_color\n",
    "        background.global_alpha = 0.3\n",
    "        background.fill_rect(drawarea_min_x, drawarea_min_y, drawarea_max_x, drawarea_max_y)\n",
    "        background.global_alpha = 1\n",
    "\n",
    "        # Draw grid and ticks\n",
    "        n_lines = 10\n",
    "        background.fill_style = 'gray'\n",
    "        background.stroke_style = '#8c8c8c'\n",
    "        background.line_width = 1\n",
    "\n",
    "        for i in range(n_lines):\n",
    "            j = i / (n_lines - 1)\n",
    "            line_x = drawarea_max_x * j + drawarea_min_x\n",
    "            line_y = drawarea_max_y * j + drawarea_min_y\n",
    "\n",
    "            # Line on the y axis\n",
    "            background.stroke_line(line_x, drawarea_min_y, line_x, drawarea_max_y + drawarea_min_y)\n",
    "\n",
    "            # Line on the x axis\n",
    "            background.stroke_line(drawarea_min_x, line_y, drawarea_max_x + drawarea_min_x, line_y)\n",
    "\n",
    "            # Draw y tick\n",
    "            background.text_align = 'right'\n",
    "            background.text_baseline = 'middle'\n",
    "            background.fill_text('{0:.2e}'.format(self.unscale_y(line_y)), drawarea_min_x * 0.95, line_y)\n",
    "\n",
    "            # Draw x tick\n",
    "            background.text_align = 'center'\n",
    "            background.text_baseline = 'top'\n",
    "            background.fill_text('{0:.2e}'.format(self.unscale_x(line_x)), line_x, drawarea_max_y + drawarea_min_y + drawarea_min_y * 0.05)\n",
    "\n",
    "    def draw(self):\n",
    "        with hold_canvas(self):\n",
    "            self.clear()\n",
    "            plot_layer = self[1]\n",
    "\n",
    "            plot_layer.save()\n",
    "\n",
    "            self.draw_background()\n",
    "\n",
    "            # Draw scatter\n",
    "            plot_layer.stroke_style = self.stroke_color\n",
    "\n",
    "            for idx in range(self.n_marks):\n",
    "                plot_layer.fill_style = self.colormap(self.color[idx])\n",
    "\n",
    "                mark_x = self.scale_x(self.x[idx])\n",
    "                mark_y = self.scale_y(self.y[idx])\n",
    "                mark_size = self.sizes[idx]\n",
    "\n",
    "                plot_layer.fill_circle(mark_x, mark_y, mark_size)\n",
    "                plot_layer.stroke_circle(mark_x, mark_y, mark_size)\n",
    "\n",
    "            plot_layer.restore()\n",
    "\n",
    "\n",
    "n_points = 1_000\n",
    "\n",
    "x = np.random.rand(n_points)\n",
    "y = np.random.rand(n_points)\n",
    "sizes = np.random.randint(2, 8, n_points)\n",
    "colors = np.random.rand(n_points) * 10 - 2\n",
    "\n",
    "plot = ScatterPlot(x, y, sizes, colors, branca.colormap.linear.viridis, stroke_color='white')\n",
    "plot"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### RoughCanvas: Give a \"hand-drawn\" style to your drawings"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "canvas = RoughCanvas()\n",
    "\n",
    "canvas.fill_style = 'red'\n",
    "canvas.stroke_style = 'blue'\n",
    "\n",
    "canvas.stroke_rect(100, 100, 100, 100)\n",
    "canvas.fill_rect(50, 50, 100, 100)\n",
    "\n",
    "canvas.stroke_circle(300, 300, 100)\n",
    "canvas.fill_circle(350, 350, 100)\n",
    "\n",
    "canvas.stroke_line(200, 200, 300, 300)\n",
    "\n",
    "canvas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Game Of Life"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def life_step(x):\n",
    "    \"\"\"Game of life step\"\"\"\n",
    "    nbrs_count = sum(np.roll(np.roll(x, i, 0), j, 1)\n",
    "                     for i in (-1, 0, 1) for j in (-1, 0, 1)\n",
    "                     if (i != 0 or j != 0))\n",
    "    return (nbrs_count == 3) | (x & (nbrs_count == 2))\n",
    "\n",
    "\n",
    "def draw(x, canvas, color='black'):\n",
    "    with hold_canvas(canvas):\n",
    "        canvas.clear()\n",
    "        canvas.fill_style = '#FFF0C9'\n",
    "        canvas.stroke_style = 'white'\n",
    "        canvas.rough_fill_style = 'solid'\n",
    "        canvas.fill_rect(-10, -10, canvas.width + 10, canvas.height + 10)\n",
    "        canvas.rough_fill_style = 'cross-hatch'\n",
    "\n",
    "        canvas.fill_style = color\n",
    "        canvas.stroke_style = color\n",
    "\n",
    "        living_cells = np.where(x)\n",
    "        \n",
    "        rects_x = living_cells[0] * n_pixels\n",
    "        rects_y = living_cells[1] * n_pixels\n",
    "\n",
    "        canvas.fill_rects(rects_x, rects_y, n_pixels)\n",
    "        canvas.stroke_rects(rects_x, rects_y, n_pixels)\n",
    "\n",
    "\n",
    "\n",
    "glider_gun =\\\n",
    "[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0],\n",
    " [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0],\n",
    " [0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1],\n",
    " [0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1],\n",
    " [1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0],\n",
    " [1,1,0,0,0,0,0,0,0,0,1,0,0,0,1,0,1,1,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0],\n",
    " [0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0],\n",
    " [0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],\n",
    " [0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]]\n",
    "\n",
    "x = np.zeros((50, 70), dtype=bool)\n",
    "x[1:10,1:37] = glider_gun\n",
    "\n",
    "n_pixels = 15\n",
    "\n",
    "canvas = RoughCanvas(width=x.shape[0]*n_pixels, height=x.shape[1]*n_pixels)\n",
    "canvas.fill_style = '#FFF0C9'\n",
    "canvas.stroke_style = 'white'\n",
    "canvas.fill_rect(0, 0, canvas.width, canvas.height)\n",
    "\n",
    "draw(x, canvas, '#5770B3')\n",
    "\n",
    "display(canvas)\n",
    "\n",
    "for _ in range(100):\n",
    "    x = life_step(x)\n",
    "    draw(x, canvas, '#5770B3')\n",
    "\n",
    "    sleep(0.1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Playing with Perlin Noise"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pattern_source = Canvas(width=50, height=50)\n",
    "\n",
    "pattern_source.fill_style = '#338ac4'\n",
    "pattern_source.fill_rect(0, 0, 50, 50)\n",
    "\n",
    "pattern_source.fill_style = '#3341c4'\n",
    "pattern_source.fill_circle(50, 50, 5)\n",
    "pattern_source.fill_circle(0, 0, 5)\n",
    "pattern_source.fill_circle(50, 0, 5)\n",
    "pattern_source.fill_circle(0, 50, 5)\n",
    "\n",
    "pattern_source.fill_style = '#33c4b5'\n",
    "pattern_source.fill_circle(25, 25, 10)\n",
    "\n",
    "\n",
    "# Quick and dirty perlin noise function\n",
    "def dgg(ix, iy, x, y):\n",
    "    random = 2920 * np.sin(ix * 21942 + iy * 171324 + 8912) * np.cos(ix * 23157 * iy * 217832 + 9758)\n",
    "    return ((x - ix)*np.cos(random) + (y - iy)*np.sin(random));\n",
    "\n",
    "\n",
    "def perlin(x,y):\n",
    "    x0, y0 = np.array(x).astype(int),np.array(y).astype(int)\n",
    "    n0, n1 = dgg(x0, y0, x, y), dgg((x0 + 1), y0, x, y)\n",
    "    ix0 = (n1 - n0) * (x - x0) + n0\n",
    "    n0, n1 = dgg(x0, (y0 + 1), x, y), dgg((x0 + 1), (y0 + 1), x, y)\n",
    "    return (((n1 - n0) * (x - x0) + n0) - ix0) * (y-y0) + ix0\n",
    "\n",
    "\n",
    "canvas = Canvas(width=500, height=500)\n",
    "canvas.global_alpha = 0.1\n",
    "pattern = canvas.create_pattern(pattern_source)\n",
    "canvas.fill_style = pattern\n",
    "display(canvas)\n",
    "\n",
    "# Some particles\n",
    "n_particles = 4100\n",
    "x = np.array(np.random.rayleigh(250, n_particles))\n",
    "y = np.array(np.random.rayleigh(250, n_particles))\n",
    "size = np.random.randint(1, 3, n_particles)\n",
    "\n",
    "\n",
    "def draw_particles():\n",
    "    canvas.fill_rects(x, y, size)\n",
    "\n",
    "\n",
    "def update_particle_locations():\n",
    "    global x, y\n",
    "    angles = perlin(x/35, y/35) * 3\n",
    "    x += np.sin(angles) * 0.3\n",
    "    y += np.cos(angles) * 0.3\n",
    "\n",
    "\n",
    "for _ in range(200):\n",
    "    update_particle_locations()\n",
    "    draw_particles()\n",
    "\n",
    "    sleep(0.01)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "widgets-tutorial",
   "language": "python",
   "name": "widgets-tutorial"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
